package zzu.xjc.http;

import zzu.xjc.http.util.JSON;
import zzu.xjc.http.util.Logger;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class HttpResponse {
    private String version = "HTTP/1.1";
    private byte[] versionBytes = version.getBytes(StandardCharsets.US_ASCII);
    private StatusCode statusCode = StatusCode.get(200);
    private final HashMap<String,String> headers = new HashMap<>();
    private List<String> setCookies;
    private String body;

    private List<ByteBuffer> bufferList;

    static HttpResponse build(){
        HttpResponse response = new HttpResponse();
        response.setHeader(Header.Server,HttpServer.ServerName);
        return response;
    };

    public void setStatusCode(int code) {
        if (!StatusCode.map.containsKey(code)){
            Logger.warn("Have no this StatusCode");
            return;
        }
        this.statusCode = StatusCode.get(code);
    }

    private static final byte[] EndOfLine = {'\r','\n'};
    private static final byte[] Space = " ".getBytes();

    List<ByteBuffer> toBuffer(BufferPool bufferPool){
        if (bufferList != null){
            return bufferList;
        }
        if (this == NotFound404){
            LinkedList<ByteBuffer> list = new LinkedList<>();
            list.add(Buffer.NotFound404());
            return list;
        }

        byte[] bodyBytes = null;
        if (body != null && body.length() > 0){
            bodyBytes = body.getBytes();
            headers.put(Header.ContentLength, String.valueOf(bodyBytes.length));
        }

        ArrayList<byte[]> list = new ArrayList<>();

        int len = 0;
        len += version.length();
        // space
        len += statusCode.codeBytes.length;
        // space
        len += statusCode.msgBytes.length;
        // \r\n
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String str = entry.getKey() + ": " + entry.getValue() + "\r\n";
            byte[] bytes = str.getBytes();
            list.add(bytes);
            len += bytes.length;
        }
        if (setCookies != null){
            for (String setCookie : setCookies) {
                byte[] bytes = setCookie.getBytes();
                list.add(bytes);
                list.add(EndOfLine);
                len += bytes.length;
                len += 2;
            }
        }
        list.add(EndOfLine);

        // \r\n
        if (bodyBytes != null){
            len += bodyBytes.length;
        }
        // \r\n
        len += 8; // space * 2  \r\n * 3

        if (len <= 1024){
            ByteBuffer buffer = bufferPool.allocate();
            buffer.put(versionBytes);
            buffer.put(Space);
            buffer.put(statusCode.codeBytes);
            buffer.put(Space);
            buffer.put(statusCode.msgBytes);
            buffer.put(EndOfLine);
            for (byte[] bytes : list) {
                buffer.put(bytes);
            }
            if (bodyBytes != null){
                buffer.put(bodyBytes);
            }
            buffer.put(EndOfLine);
            List<ByteBuffer> bufferList = new LinkedList<>();
            bufferList.add(buffer.flip());
            return bufferList;
        }

        int bufferCount = (int) Math.ceil(((float) len) / 1024);
        List<ByteBuffer> bufferList = bufferPool.allocate(bufferCount);
        Iterator<ByteBuffer> iter = bufferList.iterator();
        ByteBuffer buffer = iter.next();

        buffer.put(versionBytes);
        buffer.put(Space);
        buffer.put(statusCode.codeBytes);
        buffer.put(Space);
        buffer.put(statusCode.msgBytes);
        buffer.put(EndOfLine);

        for (byte[] bytes : list) {
            int remainingSize = buffer.capacity() - buffer.position();
            if (bytes.length < remainingSize){
                buffer.put(bytes);
                continue;
            }
            buffer.put(bytes,0,remainingSize);
            buffer = iter.next();
            buffer.put(bytes,remainingSize, bytes.length - remainingSize);
        }
        if (bodyBytes != null){
            int curr = 0;
            while (curr < bodyBytes.length){
                int bufSize = buffer.capacity() - buffer.position();
                int bytesSize = bodyBytes.length - curr;
                if (bufSize < bytesSize) {
                    // 本次不能put全部byte  Could not put all bytes this time
                    buffer.put(bodyBytes,curr,bufSize);
                    curr += bufSize;
                    buffer = iter.next();
                }else{
                    // 本次能够put全部 byte  Could put all bytes into buffer this time
                    buffer.put(bodyBytes,curr,bytesSize - curr);
                    break;
                }
            }
        }
        if (buffer.hasRemaining()){
            buffer.put(EndOfLine[0]);
        }
        if (buffer.hasRemaining()){
            buffer.put(EndOfLine[1]);
        }
        bufferList.forEach(ByteBuffer::flip);
        return bufferList;
    }
    ByteBuffer toHeaderBuffer(BufferPool bufferPool){
        ByteBuffer buffer = bufferPool.allocate();
        if (body != null && body.length() > 0){
            byte[] bodyBytes = body.getBytes();
            headers.put(Header.ContentLength, String.valueOf(bodyBytes.length));
        }
        buffer.put(versionBytes);
        buffer.put(Space);
        buffer.put(statusCode.codeBytes);
        buffer.put(Space);
        buffer.put(statusCode.msgBytes);
        buffer.put(EndOfLine);
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            buffer.put(entry.getKey().getBytes());
            buffer.put(": ".getBytes());
            buffer.put(entry.getValue().getBytes());
            buffer.put(EndOfLine);
        }
        if (setCookies != null){
            for (String setCookie : setCookies) {
                buffer.put(setCookie.getBytes());
                buffer.put(EndOfLine);
            }
        }
        buffer.put(EndOfLine);
        return buffer.flip();
    }

    public void writeText(String content){
        if (body != null){
            Logger.info("HttpResponse.writeAsJSON() Http response body is not null");
        }
        headers.put(HttpResponse.Header.ContentType,ContentType.PlainText);
        body = content;
    };
    public void writeText(String ...contents) {
        if (contents.length <= 0){
            return;
        }
        if (contents.length == 1){
            writeText(contents[0]);
            return;
        }
        StringBuilder builder = new StringBuilder(contents[0]);
        for (int i = 1; i < contents.length; i++) {
            builder.append(contents[i]);
        }
        headers.put(HttpResponse.Header.ContentType, ContentType.PlainText);
        body = builder.toString();
    }
    public void writeTextAppend(String content){
        if (body == null){
            headers.put(HttpResponse.Header.ContentType,ContentType.PlainText);
            body = content;
            return;
        }
        body = body + content;
    }
    public void writeAsJSON(Object obj){
        if (body != null){
            Logger.info("HttpResponse.writeAsJSON() Http response body is not null");
        }
        headers.put(Header.ContentType,ContentType.JSON);
        if (obj instanceof String){
            body = (String) obj;
            return;
        }
        body = JSON.stringify(obj);
    }

    public void sendRedirect(String url){
        this.setStatusCode(302);
        this.headers.put("Location",host + Http.Url.process(url));
    }
    public void sendRedirectPermanently(String url){
        this.setStatusCode(301);
        this.headers.put("Location",host + Http.Url.process(url));
    }

    HashMap<String, String> headers() {
        return this.headers;
    }

    public void setHeader(String key,String value){
        this.headers.put(key,value);
    }

    List<String> setCookies() {
        if (setCookies == null){
            setCookies = new LinkedList<>();
        }
        return setCookies;
    }


    static String host;

    static {
        try {
            host = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(version).append(" ").append(statusCode.code).append(" ").append(statusCode.msg).append("\r\n");
        headers.forEach((key, val) -> builder.append(key).append(": ").append(val).append("\r\n"));
        setCookies().forEach( setCookie -> builder.append(setCookie).append("\r\n"));
        if (body != null){
            builder.append(body);
        }
        return builder.toString();
    }

    void setBufferList(List<ByteBuffer> bufferList) {
        this.bufferList = bufferList;
    }

    /*private HttpRequest request;
    void setRequest(HttpRequest request) {
        this.request = request;
    }
    String getRequestUrl(){
        return request.url();
    }*/

    static class StatusCode {
        public static HashMap<Integer,StatusCode> map = new HashMap<>();
        static {
            map.put(200,new StatusCode(200,"OK"));
            map.put(301,new StatusCode(301,"Permanently Moved"));
            map.put(302,new StatusCode(302,"Temporarily Moved"));
            map.put(307,new StatusCode(307,"Temporary Redirect"));
            map.put(400,new StatusCode(400,"Bad Request"));
            map.put(401,new StatusCode(401,"Unauthorized"));
            map.put(403,new StatusCode(403,"Forbidden"));
            map.put(404,new StatusCode(404,"Not Found"));
            map.put(500,new StatusCode(500,"Internal Server Error"));
        }
        public final int code;
        public final String msg;
        public final byte[] codeBytes;
        public final byte[] msgBytes;
        private StatusCode(int code,String msg){
            this.code = code;
            this.msg = msg;
            this.codeBytes = String.valueOf(code).getBytes(StandardCharsets.US_ASCII);
            this.msgBytes = msg.getBytes(StandardCharsets.US_ASCII);
        }
        public static StatusCode get(int code){
            return map.get(code);
        }
    }

    static HttpResponse NotFound404 = new HttpResponse();

    static class Buffer{
        private Buffer(){}

        static ByteBuffer helloWorld(){
            byte[] bytes = ("HTTP/1.1 200 OK\r\nDate: Mon, 20 Feb 2021 09:13:59 GMT\r\nContent-Type: text/plain;charset=UTF-8\r\nVary: Accept-Encoding\r\nProxy-Connection: Keep-alive\r\n\r\nHelloWorld\r\n\r\n").getBytes();
            return ByteBuffer.wrap(bytes);
        }
        private static final ByteBuffer NotFound404 = ByteBuffer.wrap("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain;charset=UTF-8\r\nVary: Accept-Encoding\r\nProxy-Connection: Keep-alive\r\n\r\n404 Not Found\r\n\r\n".getBytes(StandardCharsets.UTF_8));

        static ByteBuffer NotFound404() {
            return NotFound404.rewind();
        }
    }
    public static class Header{
        private Header(){}
        public static String ContentType = "Content-Type";
        public static String ContentLength = "Content-Length";
        public static String Server = "Server";
    }
    public static class ContentType{
        private ContentType(){}
        public static String JSON = "application/json";
        public static String PlainText = "text/plain";
    }
}
